18 Vue3 入门
- Vue3 是 Vue.js 的下一个主要版本,它的目标是提供更快的渲染速度、更小的体积、更好的 TypeScript 支持、更好的代码组织和更好的开发体验。
- 官方文档:Vue.js - 渐进式 JavaScript 框架 | Vue.js
Vue2 和 Vue3
API 风格
Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0,
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
},
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
},
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template><script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>选项式 API (Options API)
- 使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如
data、methods和mounted。 - 选项所定义的属性都会暴露在函数内部的
this上,它会指向当前的组件实例。
组合式 API (Composition API)
- 通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。
- 在单文件组件中,组合式 API 通常会与
<script setup>搭配使用。这个setupattribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>中的导入和顶层变量/函数都能够在模板中直接使用。
该选哪一个
- 两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。
- 选项式 API 以“组件实例”的概念为中心 (即上述例子中的
this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。 - 组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
- 大致建议:
- 在学习的过程中,推荐采用更易于自己理解的风格。再强调一下,大部分的核心概念在这两种风格之间都是通用的。熟悉了一种风格以后,你也能够很快地理解另一种风格。
- 在生产项目中:
- 当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。
- 当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。
- 相比 Vue2 选项式 API,Vue3 组合式 API 有以下优点:
- 代码量变少
- 分散式维护变成集中式维护
Vue3 的优势
- 更容易维护
- 组合式 API
- 更好的 TypeScript 支持
- 更快的速度
- 重写 diff 算法
- 模版编译优化
- 更高效的组件初始化
- 更小的体积
- 良好的 TreeShaking
- 按需引入
- 更优的数据响应式
- Proxy
创建 Vue3 应用
安装 Node.js
安装最新 LTS 版本的 Node.js
scoop install nodejs-lts
fnm install --lts使用 create-vue 创建项目
pnpm create vue@latest这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
❯ pnpm create vue@latest
../.pnpm-store/v3/tmp/dlx-12704 | +1 +
../.pnpm-store/v3/tmp/dlx-12704 | Progress: resolved 1, reused 0, downloaded 1, added 1, done
Vue.js - The Progressive JavaScript Framework
√ 请输入项目名称: ... vue3-demo
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
√ 是否引入 Prettier 用于代码格式化? ... 否 / 是
正在构建项目 E:\code\vue3-demo...
项目构建完成,可执行以下命令:
cd vue3-demo
pnpm install
pnpm format
pnpm dev
❯ cd vue3-demo && pnpm i
Packages: +152
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 187, reused 134, downloaded 18, added 152, done
node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild: Running postinstall script, done in 52ms
dependencies:
+ vue 3.4.15
devDependencies:
+ @rushstack/eslint-patch 1.7.2
+ @vitejs/plugin-vue 5.0.3
+ @vue/eslint-config-prettier 8.0.0 (9.0.0 is available)
+ eslint 8.56.0
+ eslint-plugin-vue 9.21.1
+ prettier 3.2.4
+ vite 5.0.12
Done in 5.5s熟悉项目和关键文件
❯ lsd --tree --icon-theme unicode --group-directories-first -I node_modules
📂 .
├── 📂 public # 存放公共资源
│ └── 📄 favicon.ico
├── 📂 src
│ ├── 📂 assets # 项目的静态资源,比如 CSS 文件和图像文件
│ │ ├── 📄 base.css
│ │ ├── 📄 logo.svg
│ │ └── 📄 main.css
│ ├── 📂 components # 存放项目的 Vue 组件文件
│ ├── 📄 App.vue # 根组件,SFC 单文件组件
│ └── 📄 main.js # 入口文件,初始化 Vue 应用并挂载根组件、createApp 函数创建应用实例
├── 📄 .eslintrc.cjs # ESLint 的配置文件,用于定义代码规范和检查
├── 📄 .gitignore
├── 📄 .prettierrc.json # Prettier 的配置文件
├── 📄 index.html # 单页入口,提供 id 为 app 的挂载点
├── 📄 jsconfig.json
├── 📄 package.json
├── 📄 pnpm-lock.yaml
├── 📄 README.md
└── 📄 vite.config.js # Vite 构建工具的配置文件和 Vue2 项目相比,Vue3 项目的目录结构没有太大变化,但是有一些文件的内容发生了变化。
main.js中使用createApp函数创建应用实例,而不是new Vue。App.vue中- 组合式 API:
<script>标签中使用setup属性,而不是export default。 - 脚本部分使用了
ref、onMounted等组合式 API 函数,而不是data、methods等选项式 API。 - 脚本
script和模板template顺序调整。 - 模板
template不再要求唯一根元素。
- 组合式 API:
vite.config.js是 Vite 构建工具的配置文件,而不是vue.config.js。package.json中的依赖版本发生了变化。项自包文件核心依赖项变成了 Vue3.x 和 vite
IDE 开发工具
推荐使用的 IDE 是 VSCode,配合 Vue 语言特性 (Volar) 插件。该插件提供了语法高亮、TypeScript 支持,以及模板内表达式与组件 props 的智能提示。
Tip
Volar 取代了之前为 Vue 2 提供的官方 VSCode 扩展 Vetur。如果之前已经安装了 Vetur,请确保在 Vue 3 的项目中禁用它。
- TypeScript Vue Plugin 用于支持在 TS 中 import
*.vue文件。
- TypeScript Vue Plugin 用于支持在 TS 中 import
WebStorm 同样也为 Vue 的单文件组件提供了很好的内置支持。其他的 JetBrains IDE 也同样可以通过一个免费插件支持。从 2023.2 版开始,WebStorm 和 Vue 插件内置了对 Vue 语言服务器的支持。你可以在设置 > 语言和框架 > TypeScript > Vue 下将 Vue 服务设置为在所有 TypeScript 版本上使用 Volar 集成。默认情况下,Volar 将用于 TypeScript 5.0 及更高版本。
其他支持语言服务协议 (LSP) 的 IDE 也可以通过 LSP 享受到 Volar 所提供的核心功能:
浏览器开发者插件
- 文档
- Chrome 扩展商店页
nhdogjmejiglipccpnnnanhbledajbpd - Firefox 所属插件页
- Edge 扩展
olofadcdnkkjdfgjcmjaadnlehnnihnl - 独立的 Electron 应用所属插件
代码规范
- Vue 团队维护着 eslint-plugin-vue 项目,它是一个 ESLint 插件,会提供 SFC 相关规则的定义。
- 使用步骤:
pnpm add -D eslint eslint-plugin-vue,然后遵照eslint-plugin-vue的指引进行配置。- 启用 ESLint IDE 插件,比如 ESLint for VSCode,然后你就可以在开发时获得规范检查器的反馈。这同时也避免了启动开发服务器时不必要的规范检查。
- 将 ESLint 格式检查作为一个生产构建的步骤,保证你可以在最终打包时获得完整的规范检查反馈。
- (可选) 启用类似 lint-staged一类的工具在 Git commit 提交时自动执行规范检查。。
组合式 API
setup()
- 执行时机,比 beforeCreate 还要早
setup()自身并不含对组件实例的访问权,即在setup()中访问this会是undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行- 在
setup()函数中返回的对象会暴露给模板和组件实例 (需要在 setup 最后 return,才能模板中应用). - 通过 setup 语法糖可以简化代码,不再需要 return 返回。
<script setup>中的代码会在每次组件实例被创建的时候执行。- 任何在
<script setup>声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用。 import导入的内容也会以同样的方式暴露。<script setup>范围里的值也能被直接作为自定义组件的标签名使用。- 响应式状态需要明确使用响应式 API 来创建。和
setup()函数的返回值一样,ref 在模板中使用的时候会自动解包
<script setup>
// 1. 执行时机,比 beforeCreate 还要早
// 2. `setup()` 自身并不含对组件实例的访问权,即在 `setup()` 中访问 `this` 会是 `undefined`。 `setup()` 是一个新的组合 API,它是在组件实例创建之前执行的,所以在 `setup()` 中无法访问到 `this`,因为 `this` 是在组件实例创建之后才会有的
// 3. 数据 和 函数,需要在 setup 最后 return,才能模板中应用
const msg = 'Hello Vue 3.0'
const logMessage = () => console.log(msg)
</script>
<template>
<div>
<h1>{{ msg }}</h1>
<button @click="logMessage">
打印
</button>
</div>
</template><script>
// 1. 执行时机,比 beforeCreate 还要早
// 2. `setup()` 自身并不含对组件实例的访问权,即在 `setup()` 中访问 `this` 会是 `undefined`。 `setup()` 是一个新的组合 API,它是在组件实例创建之前执行的,所以在 `setup()` 中无法访问到 `this`,因为 `this` 是在组件实例创建之后才会有的
// 3. 数据 和 函数,需要在 setup 最后 return,才能模板中应用
export default {
name: 'App',
setup() {
// setup 函数中的 this 是 undefined
console.log('setup 函数执行了', this)
const msg = 'Hello Vue 3.0'
const logMessage = () => console.log(msg)
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
msg,
logMessage,
}
},
beforeCreate() {
console.log('beforeCreate 函数执行了')
},
}
</script>
<template>
<div>
<h1>{{ msg }}</h1>
<button @click="logMessage">
打印
</button>
</div>
</template>reactive()
reactive():接收一个对象类型的数据,返回一个响应式的对象。接受简单类型的数据不会报错,但是不会变成响应式的。
<script setup>
import { reactive } from 'vue'
// 1. reactive() 只能接收一个对象类型的数据,返回一个响应式的对象
const book = reactive({ title: 'Vue 3.0' })
const changeTitle = () => book.title = 'Vue 3 指南'
// 2. reactive() 接收一个基础类型的数据,但不能返回一个响应式的数据
const count = reactive(0)
// 浏览器会显示警告信息:value cannot be made reactive: 0
const increment = () => count.value++
// 浏览器会显示错误信息:App.vue: value cannot be made reactive: 0
</script>
<template>
<div>
<h2>{{ book }}</h2>
<button @click="changeTitle">
Change Title
</button>
<hr>
<h2>{{ count }}</h2>
<button @click="increment">
Increment
</button>
</div>
</template>ref()
ref(): 接收简单类型 或 复杂类型,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性.value。- ref 对象是可更改的,也就是说你可以为
.value赋予新的值。它也是响应式的,即所有对.value的操作都将被追踪,并且写操作会触发与之相关的副作用。 - 如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,请使用
shallowRef()来替代。 - 在模板中使用 ref 时,我们不需要附加
.value。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
<script setup>
import { ref } from 'vue'
const book = ref({ title: 'Vue 3.0' })
const changeTitle = () => book.value.title = 'Vue 3 指南'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<div>
<h2>{{ book.title }}</h2>
<button @click="changeTitle">
Change Title
</button>
<hr>
<h2>{{ count }}</h2>
<button @click="increment">
Increment
</button>
</div>
</template>reactive() 对比 ref()
- 都是用来生成响应式数据
- 不同点
reactive不能处理简单类型的数据ref参数类型支持更好,但是必须通过.value做访问修改ref函数内部的实现依赖于reactive函数
- 在实际工作中的推荐
- 推荐使用
ref函数,减少记忆负担,小兔鲜项目都使用ref
- 推荐使用
computed()
computed():接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过.value暴露 getter 函数的返回值。- 它也可以接受一个带有
get和set函数的对象来创建一个可写的 ref 对象。
<script setup>
import { computed, ref } from 'vue'
const array = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
const book = ref({ title: 'Vue 3.0' })
const plusOneNumber = computed(() => array.value.map(n => n + 1))
const evenNumbers = computed(() => array.value.filter(n => n % 2 === 0))
const bookTitle = computed({
get: () => book.value.title,
set: title => book.value.title = title,
})
const addNumber = () => array.value.push(array.value.length + 1)
</script>
<template>
<div>
<h2>{{ bookTitle }}</h2>
<input v-model="bookTitle">
<hr>
<p>{{ plusOneNumber }}</p>
<p>{{ evenNumbers }}</p>
<button @click="addNumber">
Add Number
</button>
</div>
</template>watch()
watch():侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。watch()默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。- 第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
- 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
- 当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
- 第三个可选的参数是一个对象,支持以下这些选项:
immediate:在侦听器创建时立即触发回调。第一次调用时旧值是undefined。deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()。onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。once: 回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (count, prevCount) => {
console.log(`Count changed: ${prevCount} -> ${count}`)
})
function autoIncrement() {
setInterval(() => {
count.value++
}, 800)
}
</script>
<template>
<div>
<h1>监视单个数据的变化</h1>
<p>Count: {{ count }}</p>
<button @click="autoIncrement">
Auto Increment
</button>
</div>
</template><script setup>
import { ref, watch } from 'vue'
const book = ref({ title: 'Vue 3.0', author: 'Evan You', year: 2020 })
const name = ref('Zhangsan')
const age = ref(18)
watch([name, age], ([name, age], [prevName, prevAge]) => {
console.log(`监视多个数据的变化:Name/Age changed: ${prevName}, ${prevAge} -> ${name}, ${age}`)
})
// 监视对象属性的变化
watch(() => [book.value.title, book.value.year], ([title, year], [prevTitle, prevYear]) => {
console.log(`监视多个数据的变化:Book changed: ${prevTitle}, ${prevYear} -> ${title}, ${year}`)
})
function updateBookInfo() {
book.value.title = 'Vue 3 指南'
book.value.year = 2021
}
function updateUser() {
name.value = 'Wangwu'
age.value = 22
}
</script>
<template>
<div>
<h1>监视多个数据的变化</h1>
<p>Book: {{ book.title }} - {{ book.year }}</p>
<p>User: {{ name }} - Age: {{ age }}</p>
<button @click="updateBookInfo">
Update Book Info
</button>
<br>
<button @click="updateUser">
Update User
</button>
</div>
</template><script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// immediate 立即执行回调函数
watch(count, (count, prevCount) => {
console.log(`Count changed: ${prevCount} -> ${count}`)
}, { immediate: true })
function autoIncrement() {
setInterval(() => {
count.value++
}, 800)
}
</script>
<template>
<div>
<h1>immediate 立即执行回调函数</h1>
<p>Count: {{ count }}</p>
<button @click="autoIncrement">
Auto Increment
</button>
</div>
</template><script setup>
import { ref, watch } from 'vue'
const book = ref({ title: 'Vue 3.0', author: 'Evan You', year: 2020 })
// 默认情况下,watch 监视的是数据的引用,如果数据的引用没有发生变化,watch 不会执行回调函数
watch(book, () => {
console.log('Book changed:', book.value)
})
// deep 深度监视
watch(book, () => {
console.log('Book changed:', book.value)
}, { deep: true })
function autoUpdateBookYear() {
const timer = setInterval(() => {
book.value.year++
if (book.value.year === 2024)
clearInterval(timer)
}, 1000)
}
</script>
<template>
<div>
<h1>deep 深度监视</h1>
<p>Book: {{ book.title }} - {{ book.year }}</p>
<button @click="autoUpdateBookYear">
Auto Update Book Year
</button>
</div>
</template>生命周期钩子
| 选项式 API | 组合式 API |
|---|---|
beforeCreate/created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
<script setup>
// beforeCreate 和 created 的相关代码
// 一律放在 setup 中执行
import { onMounted } from 'vue'
function getList() {
setTimeout(() => {
[1, 2, 3, 4, 5].forEach((item) => {
console.log(item)
})
}, 1000)
}
// 一进入页面的请求
getList()
// onMounted: 注册一个回调函数,在组件挂载完成后执行
// 如果有些代码需要在 mounted 生命周期中执行
// 写成函数的调用方式,可以调用多次,并不会冲突,而是按照顺序依次执行
onMounted(() => {
console.log('mounted 生命周期函数 - 逻辑 1')
})
onMounted(() => {
console.log('mounted 生命周期函数 - 逻辑 2')
})
</script>
<template>
<div />
</template>父子组件通信
一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。
props 父传子
- 父组件中给子组件绑定属性(通过
v-bind或:绑定),用于传递数据 - 子组件内部通过
defineProps编译器宏生成props对象,用于接收父组件传递的数据
<script setup>
import { ref } from 'vue'
import PoemContent from '@/PoemContent.vue'
const title = ref('燕歌行')
const content = ref([
{ id: 1, text: '孟冬初寒节气成,悲风入闺霜依庭。' },
{ id: 2, text: '秋蝉噪柳燕辞楹,念君行役怨边城。' },
{ id: 3, text: '君何崎岖久徂征,岂无膏沐感鹳鸣。' },
{ id: 4, text: '对君不乐泪沾缨,辟窗开幌弄秦筝。' },
{ id: 5, text: '调弦促柱多哀声,遥夜明月鉴帷屏。' },
{ id: 6, text: '谁知河汉浅且清,展转思服悲明星。' },
])
</script>
<template>
<div>
<h2>{{ title }}</h2>
<!-- 父组件绑定 text 属性,向子组件传递数据 -->
<PoemContent v-for="item in content" :key="item.id" :text="item.text" />
</div>
</template>
<style lang="css" scoped>
div {
width: 300px;
background-color: #bbe2ec;
text-align: center;
margin: 20px auto;
padding: 20px;
}
</style><script setup>
// 子组件中使用 defineProps 编译器宏定义接收的 props
defineProps({ text: String })
</script>
<template>
<li>{{ text }}</li>
</template>
<style lang="css" scoped>
li {
list-style: none;
margin: 10px 0;
}
</style>$emit 子传父
- 子组件内通过
defineEmits编译器宏生成emit方法。命名遵守驼峰规则 camelCase(enlargeText) - 子组件内部触发自定义事件并传递参数。
- 父组件中通过
v-on或@绑定事件监听,用于接收子组件触发的事件。命名遵守短横线隔开式 kebab-case(enlarge-text)。
命名规则
遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。
<script setup>
defineProps({ text: String, fontSize: String })
// 通过 defineEmits 定义一个名为 enlargeText 的事件,用于触发父组件中的事件监听
defineEmits(['enlargeText'])
</script>
<template>
<li :style="{ listStyle: 'none', margin: '10px 0' }">
{{ text }}
</li>
<!-- 通过 $emit 触发父组件中的事件监听,并传递参数 -->
<button @click="$emit('enlargeText', `${parseFloat(fontSize) + 0.1}`)">
Enlarge text
</button>
</template><script setup>
import { ref } from 'vue'
import PoemContent from '@/PoemContent.vue'
const title = ref('燕歌行')
const content = ref([
{ id: 1, text: '孟冬初寒节气成,悲风入闺霜依庭。' },
{ id: 2, text: '秋蝉噪柳燕辞楹,念君行役怨边城。' },
{ id: 3, text: '君何崎岖久徂征,岂无膏沐感鹳鸣。' },
{ id: 4, text: '对君不乐泪沾缨,辟窗开幌弄秦筝。' },
{ id: 5, text: '调弦促柱多哀声,遥夜明月鉴帷屏。' },
{ id: 6, text: '谁知河汉浅且清,展转思服悲明星。' },
])
const fontSize = ref('1.5')
</script>
<template>
<div :style="{ fontSize: `${fontSize}em` }">
<h2>{{ title }}</h2>
<!-- 父组件中通过 @ 绑定事件监听,用于接收子组件触发的事件。命名遵守短横线隔开式 kebab-case -->
<PoemContent
v-for="item in content" :key="item.id" :font-size="fontSize" :text="item.text"
@enlarge-text="fontSize = $event"
/>
</div>
</template>
<style lang="css" scoped>
div {
width: 800px;
background-color: #bbe2ec;
text-align: center;
margin: 20px;
padding: 20px;
}
</style>模板引用
- 概念:通过
ref标识 获取真实的 dom 对象或者组件实例对象 - 虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的
ref。<input ref="input"> ref是一个特殊的 attribute,和v-for章节中提到的key类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
基本使用
声明一个
ref来存放该元素的引用,必须和模板里的 ref 同名注意
你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问
input,在初次渲染时会是null。这是因为在初次渲染前这个元素还不存在呢!通过 ref 标识绑定 ref 对象到标签。
<script setup>
import { onMounted, ref } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const title = ref(null)
// 初次渲染前这个元素还不存在,所以这里的值是 null
console.log(`title: ${title.value}`)
// 也可以使用 ref 来存放 input 元素的引用
const input = ref(null)
// 初次渲染前这个元素还不存在,所以这里的值也是 null
console.log(`input: ${input.value}`)
// 页面加载后,自动聚焦 input 元素
onMounted(() => {
input.value.focus()
})
function logRefs() {
console.log(title.value, input.value)
}
</script>
<template>
<h1 ref="title">
Hello, Vue 3.0
</h1>
<input ref="input" placeholder="页面加载后,自动聚焦..." type="text">
<hr>
<button @click="logRefs">
打印 ref
</button>
</template>defineExpose
- 使用了
<script setup>的组件是默认私有的:一个父组件无法访问到一个使用了<script setup>的子组件中的任何东西,即通过模板引用或者$parent链获取到的组件的公开实例,不会暴露任何在<script setup>中声明的绑定。 - 可以通过
defineExpose编译器宏来显式指定在<script setup>组件中要暴露出去的属性和方法。
<script setup>
import { onMounted, ref } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
console.log(child.value)
console.log(`child.name: ${child.value.name}`)
console.log(`child.age: ${child.value.age}`)
console.log(`child.changeAge: ${child.value.changeAge}`)
})
</script>
<template>
<Child ref="child" />
</template><script setup>
import { ref } from 'vue'
const name = ref('Zhang san')
const age = ref(20)
const changeAge = () => age.value += 1
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({ name, age, changeAge })
</script>
<template>
<div />
</template>依赖注入
Prop 逐级透传问题
- 通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。
- 如果有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”。
provide和inject可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者 Provide。任何后代的组件树,无论层级有多深,都可以注入 Inject由父组件提供给整条链路的依赖。
provide() 提供
提供一个值,可以被后代组件注入。
要为组件后代提供数据,需要使用到
provide()函数。jsprovide(/* 注入名 */ 'message', /* 值 */ 'hello!')provide()函数接收两个参数。- 第一个参数被称为注入名 key,可以是一个字符串或是一个
Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用provide(),使用不同的注入名,注入不同的依赖值。 - 第二个参数是提供的值 value,值可以是任意类型,包括响应式的状态,比如一个 ref。
- 第一个参数被称为注入名 key,可以是一个字符串或是一个
提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
与注册生命周期钩子的 API 类似,
provide()必须在组件的setup()阶段同步调用。除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
jsximport { createApp } from 'vue' const app = createApp({}) app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
inject() 注入
注入一个由祖先组件或整个应用 (通过
app.provide()) 提供的值。要注入上层组件提供的数据,需使用
inject()函数。jsconst message = inject('message')如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,
inject()将返回undefined,除非提供了一个默认值。第二个参数是可选的,即在没有匹配到 key 时使用的默认值。
第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,你必须将
true作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。与注册生命周期钩子的 API 类似,
inject()必须在组件的setup()阶段同步调用。
<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'
// 跨层传递普通数据
const msg = 'Hello, Vite!'
provide('msg', msg)
// 跨层传递响应式数据
const count = ref(66)
provide('count', count)
const input = ref('我是 App.vue 中的 input 数据!')
provide('input', input)
// 跨层传递方法
function sayHello() {
console.log('我是 App.vue 中的 sayHello 方法!')
}
provide('sayHello', sayHello)
// 自动增加 count
function autoIncrease() {
const timer = setInterval(() => {
count.value++
if (count.value >= 100)
clearInterval(timer)
}, 500)
}
</script>
<template>
<div :style="{ width: '600px', margin: '20px', padding: '20px', border: '1px solid #000', background: '#71C9CE' }">
<h1>App.vue</h1>
<p>跨层传递普通数据:{{ msg }}</p>
<p>
跨层传递响应式数据:{{ count }}
<button @click="autoIncrease">
Auto Increase
</button>
</p>
<p>
跨层传递方法:
<button @click="sayHello">
点击我
</button>
</p>
<input v-model="input" :style="{ width: '300px', marginBottom: '20px' }">
<br>
<Child />
</div>
</template><script setup>
import GrandChild from './GrandChild.vue'
</script>
<template>
<div :style="{ padding: '20px', border: '1px solid #000', background: '#A6E3E9' }">
<h2>Child.vue</h2>
<GrandChild />
</div>
</template><script setup>
import { inject } from 'vue'
// 跨层接收普通数据
const msg = inject('msg')
// 跨层接收响应式数据
const count = inject('count')
const input = inject('input')
// 跨层接收方法
const sayHello = inject('sayHello')
</script>
<template>
<div :style="{ padding: '20px', border: '1px solid #000', background: '#CBF1F5' }">
<h3>GrandChild.vue</h3>
<p>跨层接收普通数据:{{ msg }}</p>
<p>跨层接收响应式数据:{{ count }}</p>
<p>
跨层接收方法:
<button @click="sayHello">
点击我
</button>
</p>
<p>跨层接收 input 数据:{{ input }}</p>
</div>
</template>Vue3.3 新特性
defineOptions
有
<script setup>之前,如果要定义props,emits可以轻而易举地添加一个与setup平级的属性。但是用了
<script setup>后,就没法这么干了setup属性已经没有了,自然无法添加与其平级的属性。为了解决这一问题,引入了
defineProps与defineEmits这两个宏。但这只解决了props与emits这两个属性。如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的
<script>标签。这样就会存在两个<script>标签。让人无法接受。所以在 Vue 3.3 中新引入了
defineOptions宏。这个宏可以用来直接在<script setup>中声明组件选项,而不必使用单独的<script>块。这是一个宏定义,选项将会被提升到模块作用域中,无法访问<script setup>中不是字面常数的局部变量。可以用
defineOptions定义任意的选项,props,emits,expose,slots除外(因为这些可以使用defineXxx来做到)
<script setup>
defineOptions({
name: 'App',
inheritAttrs: false,
// 更多自定义属性...
})
</script>defineModel()
在 Vue3 中,自定义组件上使用
v-model, 相当于传递一个modelValue属性,同时触发update:modelValue事件html<Child v-model="isVisible"></Child> // 相当于 <Child :modelValue="isVisible" @update:modelValue="isVisible=$event"></Child>我们需要先定义
props,再定义emits。其中有许多重复的代码。如果需要修改此值,还需要手动调用emit函数。于是乎defineModel诞生了。defineModel()返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:- 它的
.value和父组件的v-model的值同步; - 当它被子组件变更了,会触发父组件绑定的值一起更新。
- 它的
<script setup>
import { ref } from 'vue'
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
const msg = ref('Hello World!')
</script>
<template>
<div :style="{ width: '600px', margin: '20px', padding: '20px', border: '1px solid #000', background: '#71C9CE' }">
<h1>App.vue</h1>
<p>msg: {{ msg }}</p>
<span>My input</span> <input v-model="msg" :style="{ marginBottom: '20px' }">
<Child1 v-model="msg" />
<Child2 v-model="msg" />
</div>
</template><script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>
<template>
<div :style="{ marginBottom: '20px', padding: '20px', border: '1px solid #000', background: '#CBF1F5' }">
<h2>Child1.vue</h2>
<p>App.vue v-model: <strong>{{ modelValue }}</strong></p>
<span>My input</span> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</div>
</template><script setup>
const model = defineModel()
</script>
<template>
<div :style="{ padding: '20px', border: '1px solid #000', background: '#A6E3E9' }">
<h2>Child2.vue</h2>
<p>App.vue v-model: <strong>{{ model }}</strong></p>
<span>My input</span> <input v-model="model">
</div>
</template>